import os
import zlib
import shutil
from collections import deque
from .logging import loginfo, logstat, logwarning, logdebug


class GabotError(Exception):
    pass
class BuilderError(GabotError):
    pass
class BuilderNotFound(BuilderError):
    pass
class ParamError(GabotError):
    pass
class MissingParam(GabotError):
    pass
class InvalidParam(GabotError):
    pass


class Builder(object):
    _registry = {}
    PARAMS_PREFIX = None
    
    @classmethod
    def _manual_register(cls, builder, extensions):
        for ext in extensions:
            if ext in Builder._registry:
                logwarning("BUILDER", "extension %r already registered", ext)
            logdebug("BUILDER", "registering %r with %r", ext, builder)
            Builder._registry[ext] = builder
    
    @classmethod
    def register(cls, *extensions):
        """registers a builder class"""
        cls._manual_register(cls, extensions)
    
    @classmethod
    def select(cls, ext):
        """selects the appropriate builder based on the given file extension"""
        if ext not in Builder._registry:
            raise BuilderNotFound(ext)
        return Builder._registry[ext]
    
    @classmethod
    def file_crc32(cls, filename):
        crc = 0
        size = 0
        f = open(filename, "rb")
        while True:
            buf = f.read(32000)
            if not buf:
                break
            size += len(buf)
            crc = zlib.crc32(buf, crc)
        f.close()
        if crc < 0:
            crc += 2**32
        return "%08x%08x" % (size, crc)
    
    @classmethod
    def params_crc32(cls, params, prefix):
        crc = 0
        if params is not None:
            for n, v in params:
                if prefix is None or n.startswith(prefix):
                    crc = zlib.crc32(repr(n) + repr(v), crc)
        return "%08x" % (crc,)
    
    @classmethod
    def get_signature(cls, node):
        crc = cls.file_crc32(node.filename)
        if cls.PARAMS_PREFIX is not None:
            crc += cls.params_crc32(node.params, cls.PARAMS_PREFIX)
        return crc
    
    @classmethod
    def scan(cls, node):
        pass
    @classmethod
    def get_output_name(cls, node):
        pass
    @classmethod
    def build(cls, node):
        pass


class ResourceBuilder(Builder):
    def __init__(self, resource_ext, resource_builder = None):
        self.resource_ext = resource_ext
        self.resource_builder = resource_builder
    
    def scan(self, node):
        node._output = node.relname
        resfile = os.path.join(node.dirname, "%s.%s" % (node.basename, 
            self.resource_ext))
        child = node.create_child(resfile, builder = self.resource_builder)
        child._output = node._output
        node._child = child
    
    def get_signature(self, node):
        return node._child.builder.get_signature(node._child)
    
    def get_output_name(self, node):
        return None
    
    def register(self, *extensions):
        self._manual_register(self, extensions)
    
    def build(self, node):
        pass


class ChainBuilder(Builder):
    def __init__(self, outer, inner):
        self.outer = outer
        self.inner = inner
    
    def register(self, *extensions):
        self._manual_register(self, extensions)
    
    def scan(self, node):
        self.inner.scan(node)
    
    def get_signature(self, node):
        pass
    
    def get_output_name(self, node):
        self.outer.get_output_name(node)
    
    def build(self, node):
        self.outer.build(node)


class AutoBuilder(Builder):
    @classmethod
    def get_output_name(cls, node):
        return node._output


class Node(object):
    CACHE_DIR = ".gabotcache"
    _registry = {}
    
    def __init__(self, filename, builder):
        self.filename = filename
        self.dirname = os.path.dirname(filename)
        self.relname = os.path.basename(filename)
        if "." in self.relname:
            self.basename, self.fullext = self.relname.split(".", 1)
            self.ext = self.fullext.rsplit(".", 1)[-1]
        else:
            self.basename = self.relname
            self.fullext = ""
            self.ext = ""
        self.builder = builder
        self.children = set()
        self.scanned = False
        self.params = None
        self.cache_dirname = os.path.join(self.dirname, self.CACHE_DIR)
        self.cache_filename = None
        self.output = None
        self.rebuild = True
        self.success = None
    
    def __repr__(self):
        return "Node(%r)" % (self.filename,)
    
    @classmethod
    def create(cls, filename, builder = None):
        canonical = os.path.normpath(os.path.abspath(
            os.path.expanduser(os.path.expandvars(filename))))
        if canonical in cls._registry:
            return cls._registry[canonical]
        if builder is None:
            ext = canonical.split(".", 1)[-1]
            builder = Builder.select(ext)
        node = cls(canonical, builder)
        cls._registry[canonical] = node
        return node
    
    def create_child(self, filename, builder = None):
        child = self.create(filename, builder)
        self.children.add(child)
        return child
    def remove_child(self, node):
        self.children.remove(node)
    
    def scan(self):
        """scans this node"""
        os.chdir(self.dirname)
        self.scanned = True
        self.builder.scan(self)
    
    def build(self):
        """builds this node"""
        if not self.rebuild:
            if self.output is not None:
                shutil.copyfile(self.cache_filename, self.output)
            loginfo("BUILD", "skipping %s", self.filename)
            self.success = True
            return
        
        if self.children:
            if not all(child.success for child in self.children):
                self.success = False
                logstat("BUILD", "deps failed, skipping %s", self.filename)
                return
        
        if not os.path.isdir(self.cache_dirname):
            loginfo("BUILD", "creating cache director %s", self.cache_dirname)
            os.mkdir(self.cache_dirname)
            
        os.chdir(self.dirname)
        self.builder.build(self)
        self.success = True
        
        if self.success and self.output is not None:
            shutil.copyfile(self.output, self.cache_filename)
        if self.success:
            logstat("BUILD", "built %s", self.filename)
        else:
            logstat("BUILD", "failed %s", self.filename)
    
    def clean(self):
        """deletes the cached files for this node"""
        if self.output and os.path.isfile(self.output):
            os.remove(self.output)
        if not os.path.isdir(self.cache_dirname):
            return
        filelist = os.listdir(self.cache_dirname)
        deconame = "%s--" % (self.relname,)
        for fn in filelist:
            if fn.startswith(deconame):
                os.remove(os.path.join(self.cache_dirname, fn))
    
    def calc_cache_filename(self):
        sig = self.builder.get_signature(self)
        deconame = "%s--%s" % (self.relname, sig)
        self.cache_filename = os.path.join(self.cache_dirname, deconame)
    
    def calc_output_filename(self):
        rel = self.builder.get_output_name(self)
        if rel is None:
            self.output = None
        else:
            fullname = os.path.normpath(os.path.join(self.dirname, rel))
            self.output = fullname
    
    def scan_all(self):
        """scans all nodes, starting with the rootnode. this is a non-recusive 
        version of scanning, so as not to overload the stack"""
        to_scan = deque([self])
        while to_scan:
            node = to_scan.popleft()
            if not node.scanned:
                node.scan()
                node.calc_output_filename()
                node.calc_cache_filename()
                to_scan.extend(node.children)
    
    def toposort(self):
        order = []
        _toposort_rec(self, order, set())
        return order


def _toposort_rec(node, order, visited):
    visited.add(node)
    for child in node.children:
        if child not in visited:
            _toposort_rec(child, order, visited)
    order.append(node)


def mark_unchanged(nodelist):
    """marks all unchanged nodes in the nodelist; returns the number of
    changed nodes. note: the nodelist must be topologically sorted"""
    unchanged = set()
    for node in nodelist:
        node.rebuild = False
        for child in node.children:
            if child not in unchanged:
                node.rebuild = True
                break
        if node.rebuild or (node.output is not None and 
        not os.path.exists(node.cache_filename)):
            node.clean()
            node.rebuild = True
        else:
            unchanged.add(node)
    return len(nodelist) - len(unchanged)


class Namespace(object):
    """an object that provides nested namespaces to modules"""
    __slots__ = ["__parent__", "__locals__"]
    
    def __init__(self, parent = None):
        object.__setattr__(self, "__parent__", parent)
        object.__setattr__(self, "__locals__", {})
    def __repr__(self):
        items = ", ".join("%s = %r" % (n, v) for n, v in self)
        return "<Namespace: %s>" % (items,)
    def __contains__(self, name):
        return self[name] is not None
    def __dir__(self):
        names = set()
        curr = self
        while curr is not None:
            names.update(object.__getattribute__(curr, "__locals__").keys())
            curr = object.__getattribute__(curr, "__parent__")
        return list(names)
    def __iter__(self):
        for name in Namespace.__dir__(self):
            yield name, self[name]
    def __copy__(self):
        ns = Namespace()
        for name, value in self:
            ns[name] = value
        return ns
    def __delattr__(self, name):
        self[name] = None
    def __getattribute__(self, name):
        value = object.__getattribute__(self, "__locals__").get(name, AttributeError)
        if value is AttributeError:
            parent = object.__getattribute__(self, "__parent__")
            if parent is None:
                value = None
            else:
                value = getattr(parent, name)
        return value
    def __setattr__(self, name, value):
        object.__getattribute__(self, "__locals__")[name] = value
    __getitem__ = __getattribute__
    __delitem__ = __delattr__
    __setitem__ = __setattr__


class CustomParams(object):
    def __init__(self, _filename_, **params):
        self.filename = _filename_
        self.params = params
    def __repr__(self):
        return "CustomParams(%r)" % (self.filename,)




















